fix(release): consolidate release locks, prerelease logic and version truth (#1231) - #1290
Merged
Merged
Conversation
… truth (#1231) M14: add a deterministic local release lock (tools/autoflow/release-lock.ts) taken by every release-mutating autoflow command (patch-release, release, release-prepare, publish-existing, release-record). Acquisition is atomic (createNew) under the gitignored .artifacts/ scratch dir; a second local release operation fails closed and names the holder, and the lock is released on both the Deno.exit and exception paths. CI mutual exclusion keeps using the openelement-release concurrency group in autoflow-release.yml. M16: one canonical prerelease/version truth implementation in tools/lib/version.ts (import-free, so project-constants.ts stays loadable by Nitro/jiti under Node). bump-version, check-strategic-docs, check-version-anchors, check-docs-truth, check-release-truth, npm-release-verifier, release-evidence-consistency, autoflow cli/release/ version-anchors and publish-npm now import parseLineVersion / prereleaseParts / prereleaseSequence / prereleaseChannel / compareVersions / nextPatchVersion / normalizeReleaseVersion from it instead of re-rolling regexes; FIRST_TAGGED_VERSION ('0.41.0-alpha.14') now has a single copy (previously hard-coded in both autoflow/release.ts and check-docs-truth.ts). M17: the docs-truth current gate whitelist (CURRENT_DOC_ALLOWED) switches from substring matching to exact-path semantics: trailing-'/' entries are repo-anchored directory prefixes, all other entries are exact file paths. Incidental substring exemptions (e.g. docs/runbooks/supabase-migrations.md via 'migration') are now gated and pass the scan; dead entries dropped. freeze:semantics local/CI diff-semantics gap (Beta.1 carried risk): the module header now carries the precise characterization — divergence between the origin/main local base and the origin/$GITHUB_BASE_REF CI base is provably one-directional (fail closed) under the ADR-0151 train topology (origin/main is an ancestor of every dev-based PR HEAD, so the local diff is a superset of the CI diff while local amendment signals are a subset), and documents why defaulting the local base to origin/dev would be fail-open for dev→main release PRs.
Contributor
|
APIError: Insufficient Balance |
This was referenced Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1231. Part of umbrella #1155; stage issue #1288. Conforms to the #1216 canonical verdict contract (tools/gate-verdict.ts) — this PR touches no gate verdicts, only version truth, locking and whitelist semantics.
Problem
openelement-releaseconcurrency group in.github/workflows/autoflow-release.yml, but nothing stopped two local release operations from interleaving in one working copy (both rewritingproject-constants.ts, evidence records and manifests mid-plan).project-constants.ts,autoflow/version-anchors.ts, channel maps innpm-release-verifier.ts/publish-npm.ts/check-release-truth.ts, alpha-number extraction in three docs gates,normalizeReleaseVersioninautoflow/cli.ts, semver parse/compare inbump-version.tsandautoflow/release.ts);0.41.0-alpha.14(the immutable-tag policy boundary) was hard-coded twice.file.includes(entry)), exempting any path containing tokens likemigration/archiveanywhere (e.g.docs/runbooks/supabase-migrations.mdslipped through viamigration).freeze:semantics:checkcompute different diffs (local baseorigin/mainvs CI baseorigin/$GITHUB_BASE_REF) and CI additionally reads the PR body — an uncharacterized local/CI semantics gap.Owner
Release tooling / governance gates (
tools/,tools/autoflow/) — single owner: the release-toolchain surface already owningautoflow/release.ts,publish-npm.tsand the docs-truth gates.Before
FIRST_TAGGED_VERSIONduplicated inautoflow/release.ts:533andcheck-docs-truth.ts:740.legacy,sop/,docs/arch/, …) matched nothing while lookalike paths were silently exempt.After
tools/autoflow/release-lock.ts— deterministic lock at.artifacts/autoflow-release.lock(gitignored scratch) acquired atomically (createNew) by every release-mutating CLI command (patch-release,release,release-prepare,publish-existing,release-record). A second local release fails closed, naming the holder (pid/command/startedAt). Released viafinally(exception paths) and anunloadhook (Deno.exitpaths, verified to fire); a hard kill leaves a stale lock that the next run reports by name — recovery is a deliberate human act, never silent lock-breaking. CI exclusion remains the workflow concurrency group.tools/lib/version.ts—parseLineVersion/tryParseLineVersion(strictx.y.z(-label.n), no@std/semver: the module is import-free soproject-constants.tsstays loadable by Nitro/jiti under Node),prereleaseParts,prereleaseSequence,prereleaseChannel(the alpha|beta|rc dist-tag set),compareVersions,nextPatchVersion,normalizeReleaseVersion,FIRST_TAGGED_VERSION. All ten former copies now import it; established export names (parseVersion,compareVersions,nextPatchVersion,normalizeReleaseVersion) remain as thin delegates so existing call sites/tests are unchanged.CURRENT_DOC_ALLOWEDis exact-path: trailing-/entries are repo-anchored directory prefixes, other entries are exact file paths; matching exported asisCurrentDocAllowed(). Dead entries dropped; the one incidentally-exempt file (docs/runbooks/supabase-migrations.md) is now gated and passes the scan.tools/check-frozen-semantics.tsheader. Divergence is one-directional (fail closed): under the ADR-0151 train topologyorigin/mainis an ancestor of every dev-based PR HEAD (verified:git merge-base --is-ancestor origin/main origin/dev✓, 0 commits on main not in dev), so the local diff is a superset of the CI diff while local amendment signals are a subset (no PR body) —evaluate(local)=ok ⇒ evaluate(CI)=ok; a local green can never mask a red CI. The residual gap is only local false-FAIL (dev drift in the wider diff, PR-body-only citations). The header also records why defaulting the local base toorigin/devwould be fail-open for dev→main release PRs (empty local diff vs main-diff in CI).Why-not-second-owner
All four items live in the same release-toolchain/docs-gate surface and interlock through the one new module (
tools/lib/version.ts); splitting M16 from M14/M17 would create cross-PR import conflicts inautoflow/cli.tsandcheck-docs-truth.tswithout changing ownership. No product package, public API or frozen-semantics path is touched (no ADR-0151 citation required;freeze:semantics:checkpasses).Evidence
tools/lib/version.ts,release-lock.ts,isCurrentDocAllowed); green after.deno task test→ exit 0 (full suite incl.tools/and the supabase starter).deno fmt --check/deno lint→ clean.deno task docs:truth→ all 7 docs-truth gates + release-truth + version anchors (25 anchors) + recipe parity pass.deno task freeze:semantics:check→ exit 0.graph:check,arch:check,type-safety:check,package-surface:check,interface:snapshot,repo:hygiene,release:state-machine:check→ pass.deno task pack:dry-run→ 5 tarballs;deno task package-artifacts:check→ pass. No relative cross-package imports added (tools-only change).publish-existingexit 1 with the holder named; the holder's lock file is left untouched.docs:check-current/docs:truthCI gates; M14 is machinery (not a qualification) — CI mutual exclusion was and remains the workflow concurrency group, so no new CI wiring is applicable; the freeze-gap closure is a documented proof, no new gate.Scope
In:
tools/lib/version.ts(+test),tools/autoflow/release-lock.ts(+test),tools/autoflow/{cli,release,version-anchors}.ts,tools/{bump-version,project-constants,check-strategic-docs,check-version-anchors,check-docs-truth,check-release-truth,check-frozen-semantics,publish-npm}.ts,tools/lib/{npm-release-verifier,release-evidence-consistency}.ts,tools/check-docs-truth.test.ts. Out: no frozen-semantics product paths, no package/runtime changes, no new governance machinery beyond the M14 lock the issue mandates, no CI workflow changes (existing gates cover the changed checks).Risk
@std/semver-based copies on exotic inputs (build metadata, multi-part prereleases, leading zeros) — all outside the documented strictx.y.z(-label.n)line contract and now consistently rejected everywhere; all pre-existing behavior tests pass unchanged.